package com.caishi.lkx.user.userlog;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import com.caishi.lkx.exam.model.IndustryModel;
import com.caishi.lkx.exam.service.IIndustryService;
import com.caishi.lkx.user.ienum.type.UserLogSourceType;
import com.caishi.lkx.user.userlog.model.*;
import com.zzw.common.entity.impl.BaseEntity;
import com.zzw.common.exception.BizRuntimeException;
import com.zzw.common.lock.RedissonLock;

import com.caishi.lkx.user.userlog.vo.TableRow;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;

@Service
@Slf4j
public class NormalLogRecordsService {

    @Resource
    private IIndustryService industryService;

    private final CopyOnWriteArraySet<String> industryIds = new CopyOnWriteArraySet<>();

    /**
     * 数据聚合方式
     */
    public enum DataGroupBy {
        year,
        month,
        day,
        all
    }

    @Resource
    private MongoTemplate mongoTemplate;

    @PostConstruct
    public void initIndustry() {
        List<IndustryModel> industryModels = industryService.selectList(null);
        for (IndustryModel model : industryModels) {
            industryIds.add(model.getId());
        }
    }

    private Update getUpdate(UserLogSourceType sourceType, String exam, double count, double realCount) {
        var up = new Update().inc(String.format("data.%s.todayCount", sourceType), count)
                .inc(String.format("data.%s.realTodayCount", sourceType), realCount)
                .inc(String.format("data.%s.allCount", sourceType), count)
                .inc(String.format("data.%s.realAllCount", sourceType), realCount);
        if (exam != null) {
            up.inc(String.format("data.%s.exam.%s.todayCount", sourceType, exam), count)
                    .inc(String.format("data.%s.exam.%s.realTodayCount", sourceType, exam), realCount)
                    .inc(String.format("data.%s.exam.%s.allCount", sourceType, exam), count)
                    .inc(String.format("data.%s.exam.%s.realAllCount", sourceType, exam), realCount);
        }
        return up;
    }

    // 创建MongoDB表的索引
//    @EventListener(ApplicationStartedEvent.class)
    public void createdMongodbIndex() {
        // 创建基础表的索引
        var allIndustry = allIndustry(null);
        for (var type : UserLogType.values()) {
            if (type.isInd()) allIndustry.forEach(v -> normalIndex(tableName(type, v)));
            else normalIndex(tableName(type, null));
        }
        // 创建快照表的索引
        for (var type : UserLogType.values()) {
            if (type.isInd()) allIndustry.forEach(v -> imageIndex(tableName(type, v) + "_image"));
            else imageIndex(tableName(type, null) + "_image");
        }
    }

    private void normalIndex(String table) {
        var ops = mongoTemplate.indexOps(table);
        ops.ensureIndex(new Index("groupId", Sort.Direction.ASC));
        ops.ensureIndex(new Index("date", Sort.Direction.ASC));
        for (var t : UserLogSourceType.values()) {
            ops.ensureIndex(new Index("data." + t.name(), Sort.Direction.ASC));
        }
    }

    private void imageIndex(String table) {
        var ops = mongoTemplate.indexOps(table);
        ops.ensureIndex(new Index("ip", Sort.Direction.ASC));
        ops.ensureIndex(new Index("industry", Sort.Direction.ASC));
        ops.ensureIndex(new Index("exam", Sort.Direction.ASC));
        ops.ensureIndex(new Index("groupIds", Sort.Direction.ASC));
    }


    //    @EventListener(ApplicationStartedEvent.class)
    public void dropAllTable() {
        // 创建基础表的索引
        var allIndustry = allIndustry(null);
        for (var type : UserLogType.values()) {
            if (type.isInd()) allIndustry.forEach(v -> mongoTemplate.dropCollection(tableName(type, v)));
            else mongoTemplate.dropCollection(tableName(type, null));
        }
        // 创建快照表的索引
        for (var type : UserLogType.values()) {
            if (type.isInd()) allIndustry.forEach(v -> mongoTemplate.dropCollection(tableName(type, v) + "_image"));
            else mongoTemplate.dropCollection(tableName(type, null) + "_image");
        }
        createdMongodbIndex();
    }

    /**
     * 添加今日数据
     *
     * @param logType    数据类型
     * @param groupId    机构id
     * @param sourceType 记录平台
     * @param count      虚假数量
     * @param realCount  真实数量
     * @return
     */
    public NormalLogRecords addTodayData(UserLogType logType, Long groupId, UserLogSourceType sourceType, String exam, double count, double realCount) {
        var date = LocalDate.now();
        var table = tableName(logType, null);
        Query query;
        if (count > 0) {
            query = query(where("groupId").is(groupId).and("date").is(LocalDateTimeUtil.toEpochMilli(date)));
        } else {
            query = query(where("groupId").is(groupId).and("data." + sourceType + ".realAllCount").gt(0));
        }
        var updateResult = mongoTemplate.findAndModify(query, getUpdate(sourceType, exam, count, realCount), NormalLogRecords.class, table);
        if (updateResult == null && count < 0) return null;
        if (updateResult == null) {
            RLock lock = null;
            try {
                var key = String.format("logrecords:%s-%s", groupId, logType);
                lock = RedissonLock.lock(key);
                var old = mongoTemplate.findOne(query, NormalLogRecords.class, table);
                if (old == null) {
                    old = NormalLogRecords.builder()
                            .groupId(groupId)
                            .date(LocalDateTimeUtil.toEpochMilli(date))
                            .createdTime(LocalDateTimeUtil.toEpochMilli(LocalDateTime.now()))
                            .data(new Count())
                            .recordDays(1)
                            .build();
                    var last = lastNormal(logType, groupId);
                    if (last != null) {
                        old.setRecordDays(last.getRecordDays() + 1);
                        old.getData().copyLast(last.getData());
                    }
                }
                var data = old.getData().getTypeItem(sourceType);
                data.addTodayCount(count);
                data.addAllCount(count);
                data.addRealTodayCount(realCount);
                data.addRealAllCount(realCount);
                if (exam != null) {
                    data.setExam(new HashMap<>() {{
                        put(exam, new CountItem().allAll(data, false));
                    }});
                }
                return mongoTemplate.save(old, table);
            } catch (Exception e) {
                log.error(String.format("用户日志记录失败 t=%s,g=%d,s=%s,c=%f,r=%f", logType, groupId, sourceType, count, realCount), e);
            } finally {
                if (lock != null) {
                    lock.unlock();
                }
            }
        }
        return updateResult;
    }

    /**
     * 添加今日数据
     *
     * @param logType    数据类型
     * @param groupId    机构id
     * @param sourceType 记录平台
     * @param count      虚假数量
     * @param realCount  真实数量
     * @return
     */
    public IndustryLogRecords addIndustryTodayData(UserLogType logType, Long groupId, UserLogSourceType sourceType, String industry, String exam, double count, double realCount) {
        var date = LocalDate.now();
        var table = tableName(logType, industry);
        var query = query(where("groupId").is(groupId).and("date").is(LocalDateTimeUtil.toEpochMilli(date)));
        var updateResult = mongoTemplate.findAndModify(query, getUpdate(sourceType, exam, count, realCount), IndustryLogRecords.class, table);
        if (updateResult == null) {
            RLock lock = null;
            try {
                var key = String.format("logrecords:%s-%s-%s", groupId, logType, industry);
                lock = RedissonLock.lock(key);
                var old = mongoTemplate.findOne(query, IndustryLogRecords.class, table);
                if (old == null) {
                    old = new IndustryLogRecords();
                    old.setIndustry(industry);
                    old.setCreatedTime(LocalDateTimeUtil.toEpochMilli(LocalDateTime.now()));
                    old.setGroupId(groupId);
                    old.setDate(LocalDateTimeUtil.toEpochMilli(date));
                    old.setRecordDays(1);
                    var last = lastNormal(logType, groupId, industry);
                    if (last != null) {
                        old.setRecordDays(last.getRecordDays() + 1);
                        old.getData().copyLast(last.getData());
                    }
                }
                var data = old.getData().getTypeItem(sourceType);
                data.addAllCount(count);
                data.addTodayCount(count);
                data.addRealAllCount(realCount);
                data.addRealTodayCount(realCount);
                if (exam != null) {
                    data.setExam(new HashMap<>() {{
                        put(exam, new CountItem().allAll(data, false));
                    }});
                }
                return mongoTemplate.save(old, table);
            } catch (Exception e) {
                log.error(String.format("用户日志记录失败 t=%s,g=%d,s=%s,c=%f,r=%f,i=%s,e=%s", logType, groupId, sourceType, count, realCount, industry, exam), e);
            } finally {
                if (lock != null) {
                    lock.unlock();
                }
            }
        }
        return updateResult;
    }

    /**
     * 保存记录快照
     *
     * @param userId
     * @param ip
     */
    public void saveRecordImage(UserLogType logType, String industry, String exam, List<Long> groupIds, String userId, String ip, Object image) {
        var table = tableName(logType, industry) + "_image";
        var recordImage = RecordsImage.builder().createdTime(LocalDateTime.now()).ip(ip).industry(industry).exam(exam).groupIds(groupIds).userId(userId).image(image).build();
        mongoTemplate.save(recordImage, table);
    }

    public List<String> allIndustry(Long groupId) {
        List<String> list = industryService.selectList(industryService.wrappers().select(IndustryModel::getId)).stream().map(BaseEntity::getId).collect(Collectors.toList());
        list.add("gjtk");
        return list;
    }

    /**
     * 获取机构数据总览
     *
     * @param groupId
     * @param sourceTypes
     * @return
     */
    public TableRow groupRecordAll(Long groupId, List<UserLogSourceType> sourceTypes, boolean setExam) {
        // 访问量
        var accessLog = groupOneLogTypeRecordAll(groupId, UserLogType.accessLog, sourceTypes, setExam);
        // 使用人数
        var useLog = groupOneLogTypeRecordAll(groupId, UserLogType.useLog, sourceTypes, setExam);
        // 试卷使用量
        var paperUseLog = groupAllIndustryRecords(groupId, UserLogType.paperUseLog, sourceTypes, setExam);
        // 试题使用量
        var questionUseLog = groupAllIndustryRecords(groupId, UserLogType.questionUseLog, sourceTypes, setExam);
        var registerData = groupOneLogTypeRecordAll(groupId, UserLogType.registerLog, sourceTypes, setExam);
        var registerNewData = new Count();
        registerNewData.addAll(registerData, false);
        // 使用时长
        var timeUseLog = groupOneLogTypeRecordAll(groupId, UserLogType.timeUseLog, sourceTypes, setExam);
        // 下载次数
        var downloadLog = groupOneLogTypeRecordAll(groupId, UserLogType.downloadLog, sourceTypes, setExam);
        // 平均使用时长
        return TableRow.builder()
                .accessLog(accessLog)
                .useLog(useLog)
                .paperUseLog(paperUseLog)
                .questionUseLog(questionUseLog)
                .registerLogNew(registerData)
                .registerLog(registerData)
                .timeUseLog(timeUseLog)
                .timeUseLogAvg(computedTimeUserLog1(useLog, timeUseLog))
                .downloadLog(downloadLog).build();
    }

    /**
     * 计算平均时长
     *
     * @return
     */
    private Count computedTimeUserLog1(Count timeUseLog, Count useLog) {
        BiFunction<CountItem, CountItem, CountItem> k = (ul, ut) -> new CountItem()
                .addAllCount(ut.getAllCount() / Math.max(1, ul.getAllCount()))
                .addRealAllCount(ut.getAllCount() / Math.max(1, ul.getAllCount())).
                addTodayCount(ut.getTodayCount() / Math.max(1, ul.getTodayCount()))
                .addRealTodayCount(ut.getRealTodayCount() / Math.max(1, ul.getRealTodayCount()));
        return new Count().setCount(UserLogSourceType.PcZhiYe, k.apply(timeUseLog.getPcZhiYe(), useLog.getPcZhiYe()))
                .setCount(UserLogSourceType.PcGjtk, k.apply(timeUseLog.getPcGjtk(), useLog.getPcGjtk()))
                .setCount(UserLogSourceType.AppZhiYe, k.apply(timeUseLog.getAppZhiYe(), useLog.getAppZhiYe()))
                .setCount(UserLogSourceType.AppGjtk, k.apply(timeUseLog.getAppGjtk(), useLog.getAppGjtk()))
                //.setCount(UserLogSourceType.WpZhiYeApp, k.apply(timeUseLog.getWpZhiYeApp(), useLog.getWpZhiYeApp()))
                .setCount(UserLogSourceType.WpZSZhiYe, k.apply(timeUseLog.getWpZSZhiYe(), useLog.getWpZSZhiYe()))
                .setCount(UserLogSourceType.WpZSGjtk, k.apply(timeUseLog.getWpZSGjtk(), useLog.getWpZSGjtk()));
    }

    /**
     * 机构数据详情
     *
     * @param groupId
     * @param sourceTypes
     * @param start
     * @param end
     * @param groupBy
     * @return
     */
    public Map<LocalDate, TableRow> groupRecordList(Long groupId, List<UserLogSourceType> sourceTypes, LocalDate start, LocalDate end, DataGroupBy groupBy, boolean setExam) {
        var times = groupByTimeList(start, end, groupBy);
        // 访问量
        var accessLog = groupRecordList(groupId, UserLogType.accessLog, sourceTypes, start, end, groupBy, setExam);
        // 使用人数
        var useLog = groupRecordList(groupId, UserLogType.useLog, sourceTypes, start, end, groupBy, setExam);
        // 试卷使用量
        var paperUseLog = groupIndustryRecordListAll(groupId, UserLogType.paperUseLog, sourceTypes, start, end, groupBy, setExam);
        // 试题使用量
        var questionUseLog = groupIndustryRecordListAll(groupId, UserLogType.questionUseLog, sourceTypes, start, end, groupBy, setExam);
        // 注册数据
        var registerData = groupRecordList(groupId, UserLogType.registerLog, sourceTypes, start, end, groupBy, setExam);
        // 使用时长数据
        var timeUseLog = groupRecordList(groupId, UserLogType.timeUseLog, sourceTypes, start, end, groupBy, setExam);
        // 下载次数
        var downloadLog = groupRecordList(groupId, UserLogType.downloadLog, sourceTypes, start, end, groupBy, setExam);
        Map<LocalDate, TableRow> res = new LinkedHashMap<>();
        for (var time : times) {
            var registerItem = questionUseLog.get(time);
            res.put(time, TableRow.builder()
                    .accessLog(accessLog.get(time))
                    .useLog(useLog.get(time))
                    .paperUseLog(paperUseLog.get(time))
                    .questionUseLog(registerItem)
                    .registerLogNew(registerData.get(time)).timeUseLog(timeUseLog.get(time))
                    .registerLog(registerData.get(time)).timeUseLog(timeUseLog.get(time))
                    .timeUseLogAvg(computedTimeUserLog1(useLog.get(time), timeUseLog.get(time)))
                    .downloadLog(downloadLog.get(time)).build());
        }
        return res;
    }


    /**
     * 获取通用记录的最后数据统计
     *
     * @param groupId
     * @param logType
     * @param sourceTypes
     * @return
     */
    public @NotNull Count groupOneLogTypeRecordAll(Long groupId, UserLogType logType, List<UserLogSourceType> sourceTypes, boolean setExam) {
        if (logType.equals(UserLogType.timeUseLogAvg)) {
            return getTimeAvg(groupId, sourceTypes, setExam);
        } else if (logType.equals(UserLogType.registerLogNew)) {
            NormalLogRecords normalLogRecords = lastNormal(UserLogType.registerLog, groupId);
            return groupRecordAllByRow(normalLogRecords, sourceTypes, setExam);
        } else {
            if (logType.isInd()) {
                return groupAllIndustryRecords(groupId, logType, sourceTypes, setExam);
            } else {
                NormalLogRecords normalLogRecords = lastNormal(logType, groupId);
                return groupRecordAllByRow(normalLogRecords, sourceTypes, setExam);
            }

        }

    }

    private Count getTimeAvg(Long groupId, List<UserLogSourceType> sourceTypes, boolean setExam) {
        NormalLogRecords userLogRecords = lastNormal(UserLogType.useLog, groupId);
        NormalLogRecords timeLogRecords = lastNormal(UserLogType.timeUseLog, groupId);
        Count count = groupRecordAllByRow(userLogRecords, sourceTypes, setExam);
        Count count1 = groupRecordAllByRow(timeLogRecords, sourceTypes, setExam);
        return computedTimeUserLog1(count, count1);
    }

    /**
     * 获取通用记录的最后数据统计
     *
     * @param row
     * @param sourceTypes
     * @return
     */
    public @NotNull Count groupRecordAllByRow(NormalLogRecords row, List<UserLogSourceType> sourceTypes, boolean setExam) {
        if (row == null) return new Count();
        return sourcesData(row.getData(), sourceTypes, setExam);
    }

    /**
     * 获取全行业统计数据
     *
     * @param groupId
     * @param logType
     * @param sourceTypes
     * @return
     */
    public @NotNull Count groupAllIndustryRecords(Long groupId, UserLogType logType, List<UserLogSourceType> sourceTypes, boolean setExam) {
        var allIndustry = allIndustry(groupId);
        var res = new Count();
        var k = allIndustry.parallelStream().map(v -> groupOneIndustryRecords(groupId, logType, v, sourceTypes, setExam)).toList();
        k.forEach(v -> res.addAll1(v, setExam));
        return res;
    }

    /**
     * 获取行业数据详情
     *
     * @param groupId
     * @param logType
     * @param industry
     * @param sourceTypes
     * @return
     */
    public @NotNull Count groupOneIndustryRecords(Long groupId, UserLogType logType, String industry, List<UserLogSourceType> sourceTypes, boolean setExam) {
        var last = lastNormal(logType, groupId, industry);
        if (last == null) return new Count();
        return sourcesData(last.getData(), sourceTypes, setExam);
    }

    /**
     * 获取通用记录的最后数据统计，按时间统计  行业分组类型
     *
     * @param groupId
     * @param logType
     * @param sourceTypes
     * @param start
     * @param end
     * @param groupBy
     * @return
     */
    public @NotNull Map<LocalDate, Count> groupIndustryRecordListAll(Long groupId, UserLogType logType, List<UserLogSourceType> sourceTypes, LocalDate start, LocalDate end, DataGroupBy groupBy, boolean setExam) {
        Assert.notNull(groupId);
        Assert.notNull(start);
        Assert.notNull(end);
        var allIndustry = allIndustry(groupId);
        var allData = allIndustry.parallelStream().map(v -> groupIndustryRecordList(groupId, logType, v, start, end)).flatMap(Collection::stream).toList();
        return listGroup(allData, sourceTypes, start, end, groupBy, setExam);
    }

    /**
     * 获取通用记录的最后数据统计，按时间统计  行业分组类型
     *
     * @param groupId
     * @param logType
     * @param start
     * @param end
     * @return
     */
    public @NotNull List<IndustryLogRecords> groupIndustryRecordList(Long groupId, UserLogType logType, String industry, LocalDate start, LocalDate end) {
        Assert.notNull(groupId);
        Assert.notNull(start);
        Assert.notNull(end);
        Assert.notNull(industry);
        var table = tableName(logType, industry);
        var criteria = where("groupId").is(groupId).and("industry").is(industry);
        if (start != null && end != null) {
            criteria.and("date").gte(LocalDateTimeUtil.toEpochMilli(start)).lte(LocalDateTimeUtil.toEpochMilli(end));
        } else if (end != null) {
            criteria.and("date").lte(LocalDateTimeUtil.toEpochMilli(end));
        } else if (start != null) {
            criteria.and("date").gte(LocalDateTimeUtil.toEpochMilli(start));
        }
        return mongoTemplate.find(query(criteria), IndustryLogRecords.class, table);
    }

    /**
     * 获取通用记录的最后数据统计，按时间统计  行业分组类型
     *
     * @param groupId
     * @param logType
     * @param sourceTypes
     * @param start
     * @param end
     * @param groupBy
     * @return
     */
    public @NotNull Map<LocalDate, Count> groupIndustryRecordList(Long groupId, UserLogType logType, List<UserLogSourceType> sourceTypes, String industry, LocalDate start, LocalDate end, DataGroupBy groupBy, boolean setExam) {
        var rows = groupIndustryRecordList(groupId, logType, industry, start, end);
        return listGroup(rows, sourceTypes, start, end, groupBy, setExam);
    }


    /**
     * 获取通用记录的最后数据统计，按时间统计
     *
     * @param groupId
     * @param logType
     * @param start
     * @param end
     * @return
     */
    public @NotNull List<NormalLogRecords> groupRecordList(Long groupId, UserLogType logType, LocalDate start, LocalDate end) {
        Assert.notNull(groupId);
        Assert.notNull(start);
        Assert.notNull(end);
        var table = tableName(logType, null);
        var criteria = where("groupId").is(groupId);
        if (start != null && end != null) {
            criteria.and("date").gte(LocalDateTimeUtil.toEpochMilli(start)).lte(LocalDateTimeUtil.toEpochMilli(end));
        } else if (start != null) {
            criteria.and("date").gte(LocalDateTimeUtil.toEpochMilli(start));
        } else if (end != null) {
            criteria.and("date").lte(LocalDateTimeUtil.toEpochMilli(end));
        }
        return mongoTemplate.find(query(criteria), NormalLogRecords.class, table);
    }


    /**
     * 获取通用记录的最后数据统计，按时间统计
     *
     * @param groupId
     * @param logType
     * @param sourceTypes
     * @param start
     * @param end
     * @param groupBy
     * @return
     */
    public @NotNull Map<LocalDate, Count> groupRecordList(Long groupId, UserLogType logType, List<UserLogSourceType> sourceTypes, LocalDate start, LocalDate end, DataGroupBy groupBy, boolean setExam) {
        if (logType.equals(UserLogType.timeUseLogAvg)) {
            var times = groupByTimeList(start, end, groupBy);
            var useLog = groupRecordList(groupId, UserLogType.useLog, sourceTypes, start, end, groupBy, setExam);
            var timeUseLog = groupRecordList(groupId, UserLogType.timeUseLog, sourceTypes, start, end, groupBy, setExam);
            Map<LocalDate, Count> res = new LinkedHashMap<>();
            for (var time : times) {
                res.put(time, computedTimeUserLog1(useLog.get(time), timeUseLog.get(time)));
            }
//            Map<LocalDate, Count> map = new LinkedHashMap<>();
//            var rows = groupRecordList(groupId, UserLogType.useLog, start, end);
//            var rows2 = groupRecordList(groupId, UserLogType.timeUseLog, start, end);
//            Map<LocalDate, Count> localDateCountMap = listGroup(rows, sourceTypes, start, end, groupBy, setExam);
//            Map<LocalDate, Count> localDateCountMap1 = listGroup(rows2, sourceTypes, start, end, groupBy, setExam);
//            List<LocalDate> list = localDateCountMap.keySet().stream().toList();
//            for (int i = 0; i < list.size(); i++) {
//                LocalDate date = list.get(i);
//                map.put(date,computedTimeUserLog1(localDateCountMap.get(date),localDateCountMap1.get(date)));
//            }
            return res;
        } else if (logType.equals(UserLogType.registerLogNew)) {
            var rows = groupRecordList(groupId, UserLogType.registerLog, start, end);
            return listGroup(rows, sourceTypes, start, end, groupBy, setExam);
        } else {
            if (logType.isInd()) {
                Map<LocalDate, Count> map = groupIndustryRecordListAll(groupId, logType, sourceTypes, start, end, groupBy, setExam);
                return map;
            } else {
                var rows = groupRecordList(groupId, logType, start, end);
                return listGroup(rows, sourceTypes, start, end, groupBy, setExam);
            }
        }

    }


    /**
     * 列表数据聚合
     *
     * @param rows
     * @param sourceTypes
     * @param start
     * @param end
     * @param groupBy
     * @return
     */
    public Map<LocalDate, Count> listGroup(List<? extends NormalLogRecords> rows, List<UserLogSourceType> sourceTypes, LocalDate start, LocalDate end, DataGroupBy groupBy, boolean setExam) {
        var timeList = groupByTimeList(start, end, groupBy);
        Map<LocalDate, Count> res = new LinkedHashMap<>();
        for (var t : timeList) res.put(t, new Count());
        for (var row : rows) {
            var t = timeIn(timeList, row.getDateT(), groupBy);
            if (t == null) continue;
            var data = res.get(t);
            var da = sourcesData(row.getData(), sourceTypes, setExam);
            data.addAll(da, setExam);
        }
        return listGroupZero(res, timeList);
    }

    /**
     * 修改记录
     *
     * @param logType
     * @param records
     */
    public void updateRecord(UserLogType logType, NormalLogRecords records) {
        if (logType.isInd() && records instanceof IndustryLogRecords ir) {
            mongoTemplate.save(records, tableName(logType, ir.getIndustry()));
        } else {
            mongoTemplate.save(records, tableName(logType, null));
        }
    }

    /**
     * 修改某个时间之后所有时间记录的allCount
     *
     * @param logType
     * @param groupId
     * @param industryId
     * @param time
     * @param sourceType
     * @param count
     */
    public void updateRecordValueAtBeforeTime(UserLogType logType, Long groupId, String industryId, LocalDate time, UserLogSourceType sourceType, double count) {
        if (count == 0) return;
        var table = tableName(logType, logType.isInd() ? industryId : null);
        var query = query(where("groupId").is(groupId).and("date").gt(LocalDateTimeUtil.toEpochMilli(time)));
        mongoTemplate.upsert(query, new Update().inc(String.format("data.%s.allCount", sourceType), count), table);
    }

    public Map<LocalDate, Count> listGroupZero(Map<LocalDate, Count> data, List<LocalDate> timeList) {
        LocalDate last = null;
        for (var time : timeList) {
            var line = data.get(time);
            if (last != null && line.getAll().getAllCount() == 0) {
                var lastLine = data.get(last);
                line.copyLast(lastLine);
            }
            last = time;
        }
        return data;
    }

    /**
     * 根据分组规则获取时间列表中的时间
     *
     * @param list
     * @param time
     * @param groupBy
     * @return
     */
    public LocalDate timeIn(List<LocalDate> list, LocalDate time, DataGroupBy groupBy) {
        for (var t : list) {
            if (groupBy == DataGroupBy.all) return t;
            if (t.getYear() == time.getYear()) {
                if (groupBy == DataGroupBy.year) return t;
                if (t.getMonth() == time.getMonth()) {
                    if (groupBy == DataGroupBy.month) return t;
                    if (t.getDayOfMonth() == time.getDayOfMonth()) return t;
                }
            }
        }
        return null;
    }

    /**
     * 根据开始时间 结束时间和分组规则获取时间列表
     *
     * @param start
     * @param end
     * @param groupBy
     * @return
     */
    public List<LocalDate> groupByTimeList(LocalDate start, LocalDate end, DataGroupBy groupBy) {
        var res = new LinkedList<LocalDate>();
        var s = start;
        var c = true;
        while (c) {
            res.add(s);
            if (groupBy == DataGroupBy.day) s = s.plusDays(1);
            else if (groupBy == DataGroupBy.month) s = s.plusMonths(1);
            else if (groupBy == DataGroupBy.year) s = s.plusYears(1);
            else break;
            c = !s.isAfter(end);
            if (!c) {
                if (groupBy == DataGroupBy.month) {
                    c = s.getYear() <= end.getYear() && s.getMonthValue() <= end.getMonthValue();
                } else if (groupBy == DataGroupBy.year) {
                    c = s.getYear() <= end.getYear();
                }
            }
        }
        return res;
    }

    /**
     * 将记录中的数据按来源类型计算结果
     *
     * @param data
     * @param sourceTypes
     * @return
     */
    public Count sourcesData(Count data, List<UserLogSourceType> sourceTypes, boolean setExam) {
        return data.sourceData(sourceTypes, setExam);
    }

    /**
     * 获取通用记录的最后记录
     *
     * @param logType
     * @param groupId
     * @return
     */
    public @Null NormalLogRecords lastNormal(UserLogType logType, Long groupId) {

        var table = tableName(logType, null);
        var criteria = where("groupId").is(groupId).and("date").gte(LocalDate.now().minusDays(30));
        var query = query(criteria).limit(1).with(Sort.by(Sort.Order.desc("date")));
        var row = mongoTemplate.findOne(query, NormalLogRecords.class, table);
        if (row == null) {
            row = mongoTemplate.findOne(query(where("groupId").is(groupId)).limit(1).with(Sort.by(Sort.Order.desc("date"))), NormalLogRecords.class, table);
        }
        return row;
    }

    /**
     * 获取行业记录类型的最后记录
     *
     * @param logType
     * @param groupId
     * @param industry
     * @return
     */
    public @Null IndustryLogRecords lastNormal(UserLogType logType, Long groupId, String industry) {
        var table = tableName(logType, industry);
        var criteria = where("groupId").is(groupId).and("date").gte(LocalDateTimeUtil.toEpochMilli(LocalDate.now().minusDays(30)));
        var query = query(criteria).limit(1).with(Sort.by(Sort.Order.desc("date")));
        var row = mongoTemplate.findOne(query, IndustryLogRecords.class, table);
        if (row == null) {
            row = mongoTemplate.findOne(query(where("groupId").is(groupId).and("industry").is(industry)).limit(1).with(Sort.by(Sort.Order.desc("date"))), IndustryLogRecords.class, table);
        }
        return row;
    }


    private String tableName(UserLogType logType, String industry) {
        if (logType.isInd()) {
            Assert.notNull(industry);
            boolean contains = industryIds.contains(industry);
            if (contains) {
                return logType.getTable() + "_" + industry;
            } else {
                IndustryModel industryModel = industryService.getById(industry);
                if (null != industryModel) {
                    industryIds.add(industry);
                    return logType.getTable() + "_" + industry;
                }
                throw new BizRuntimeException();
            }

        }
        return logType.getTable();
    }
}
